/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.maven.bundle;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.rmi.ServerException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import offset.nodes.maven.RepositoryMojo;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;

/**
 * Plugin to manage translations in the repository.
 *
 * The use case:
 *
 * 1. Create default language bundle in the software. Add properties to it. 
 * 2. Copy the information to the repository.
 * 3. Add translations in the repository.
 * 4. Generate bundles from repository
 * 
 * @goal generate-bundles
 * @phase generate-resources
 * 
 * @author Walter L�tgenau
 */
public class BundleMojo extends RepositoryMojo {

    public static final String BUNDLE_EXTENSION = "properties";
    public static final String LANGUAGE_DELIMITER = "_";
    public static final String EXTENSION_DELIMITER = ".";
    /**
     * The location of the generated bundle files
     * @parameter expression="${project.build.directory}"
     * @required
     */
    private File outputDirectory;
    /**
     * Use this search path to restrict searches for bundles
     * @parameter expression="${nodes.repository.projectPath}"
     */
    private String searchPath;
    /**
     * The module's groupId
     * @parameter expression="${project.groupId}"
     * @required
     */
    private String groupId;
    /**
     * The module's artifactId
     * @parameter expression="${project.artifactId}"
     * @required
     */
    private String artifactId;
    /**
     * The project
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    protected MavenProject project;
    /**
     * The user name in the repository
     * @parameter expression="${nodes.repository.userName}"
     * @required
     */
    private String userName;
    /**
     * The password of the user
     * @parameter expression="${nodes.repository.password}"
     * @required
     */
    private String password;
    /**
     * The RMI URL of the repository
     * @parameter expression="${nodes.repository.rmiUrl}"
     */
    private String rmiUrl;
    
    Collection<Node> bundles;
    HashMap<String, BundleEntries> bundleEntriesMap;
    Collection<BundleInfo> bundlesFound;
    Session session;
    
    class BundleEntries {
        List<Node> list = new ArrayList<Node>();
        HashMap<String, Integer> map = new HashMap<String, Integer>();
        
        /**
         * The bundle entries contain an entry of the given name.
         * 
         * @param name the name
         * @return the result
         */
        public boolean containsName(String name) {
            return map.containsKey(name);
        }
        
        /**
         * Add a node to the bundle entry representation.
         * 
         * Add it to the end of the list.
         * 
         * @param value the node
         * @throws RepositoryException 
         */
        public void add(Node value) throws RepositoryException {
            list.add(value);
            map.put(value.getName(), new Integer(list.size() - 1));
        }
        
        /**
         * Insert a node to the bundle entry representation.
         * 
         * Add it to the lexicographically correct name position within the list.
         * 
         * @param value the node
         * @throws RepositoryException 
         */
        public void insert(Node value) throws RepositoryException {
            int insertedAt = -1;
            
            for (int i = 0; i < list.size(); i++) {
                if (value.getName().compareTo(list.get(i).getName()) < 0) {
                    list.add(i, value);
                    insertedAt = i;
                    break;
                }
            }
            
            if (insertedAt < 0) {
                list.add(value);
                insertedAt = list.size() -1;
            }
            
            map.put(value.getName(), new Integer(insertedAt));
        }
        
        /**
         * Return the node of the given name.
         * 
         * @param name the name
         * @return the node
         */
        public Node get(String name) {
            if (!containsName(name))
                return null;
            
            return list.get(map.get(name).intValue());
        }
        
        /**
         * Return the list of all bundle entries
         * 
         * @return  the list
         */
        public List<Node> getList() {
            return list;
        }
    }

    public BundleMojo() {
        this.searchPath = "";

        this.bundlesFound = new LinkedList();
    }

    /**
     * Read the information from the repository. Update it from the resources, e.g. add new bundle entries.
     * Reread the now complete information, generate the bundles.
     * 
     * @throws MojoExecutionException
     */
    public void execute()
            throws MojoExecutionException {
        try {
            session = initSession(userName, password, rmiUrl);
            
            readRepository();
            updateRepository();

            //readRepository();
            updateBundles();
        } catch (ServerException se) {
            getLog().info("Could not connect to the JCR repository");            
        } catch (Exception ex) {
            throw new MojoExecutionException(ex.getMessage(), ex);
        }
    }

    /**
     * Read the bundles from the repository. For each bundles, read the bundle entries from the
     * repository.
     * @throws RepositoryException
     */
    protected void readRepository()
            throws RepositoryException {
        this.bundles = getBundles();
        this.bundleEntriesMap = new HashMap();
        for (Node bundle : this.bundles) {
            this.bundleEntriesMap.put(bundle.getName(), getBundleEntries(bundle));
        }
    }
    
    /**
     * Order the new bundle entry lexicographically into the sibling entries.
     * 
     * @param bundle the parent bundle
     * @param newBundleEntry the new entry
     * @throws RepositoryException 
     */
    protected void orderBundleEntry(Node bundle, Node newBundleEntry) throws RepositoryException {
        BundleEntries entries = bundleEntriesMap.get(bundle.getName());
        
        for (Node bundleEntry : entries.getList()) {
            if (bundleEntry.getName().compareTo(newBundleEntry.getName()) > 0) {
                bundle.orderBefore(newBundleEntry.getName(), bundleEntry.getName());
                break;
            }
        }
    }

    /**
     * Update the repository. Usually, this will just be some new properties, which
     * must be created in the software to be sure, they will match. If there are no
     * translations in the repository for the particular language, in addition the
     * language entries will be added. However, the repository does always take
     * precedence.
     *
     * @throws RepositoryException
     * @throws IOException
     */
    protected void updateRepository()
            throws RepositoryException, IOException {
        List resources = this.project.getResources();
        Iterator i = resources.iterator();
        Resource resource;
        while (i.hasNext()) {
            resource = (Resource) i.next();

            for (Node bundle : this.bundles) {
                if ((resource.getDirectory() != null) && (bundle.hasProperty(BundleConstants.PROP_PACKAGE_PATH))) {
                    String bundleDir = resource.getDirectory() + File.separator + packagePathToFilePath(bundle.getProperty(BundleConstants.PROP_PACKAGE_PATH).getString());
                    File bundleFile = new File(bundleDir + File.separator + bundle.getName() + "." + "properties");
                    if (bundleFile.exists()) {
                        BundleEntries bundleEntries = this.bundleEntriesMap.get(bundle.getName());

                        Properties bundleProperties = new Properties();
                        bundleProperties.load(new FileInputStream(bundleFile));
                        Enumeration propertyNames = bundleProperties.propertyNames();
                        ArrayList<String> propertyNamesList = new ArrayList<String>(bundleProperties.size());
                        
                        // sort the names alphabetically
                        while(propertyNames.hasMoreElements()) 
                            propertyNamesList.add((String) propertyNames.nextElement());
                        Collections.sort(propertyNamesList);

                        Iterator<String> names = propertyNamesList.iterator();
                        boolean isEmpty = bundleEntries.getList().size() == 0;
                        while (names.hasNext()) {
                            String propertyName = names.next();
                            if (!bundleEntries.containsName(propertyName)) {
                                Node bundleEntry = addNewBundleEntry(bundle, propertyName, bundleProperties.getProperty(propertyName));
                                if (!isEmpty) { // if not initial load
                                    orderBundleEntry(bundle, bundleEntry);
                                }
                                
                                bundleEntries.insert(bundleEntry);
                            }
                        }

                        resource.addExclude(packagePathToFilePath(bundle.getProperty(BundleConstants.PROP_PACKAGE_PATH).getString()) + "/" + bundleFile.getName());

                        updateOtherLanguages(resource, bundle, new File(bundleDir));

                        this.bundlesFound.add(new BundleInfo(resource, bundle));
                    }
                }
            }
        }

        this.session.save();
    }

    /**
     * Create a file path from the package path.
     *
     * @param packagePath
     * @return
     */
    protected String packagePathToFilePath(String packagePath) {
        return packagePath.replaceAll("\\.", BundleConstants.PATH_SEPARATOR);
    }

    /**
     * If there are already some translations not present in the repository, add them to the repository.
     * If already present, do nothing, repository takes precedence.
     *
     * @param resource
     * @param bundle
     * @param bundleDir
     * @throws FileNotFoundException
     * @throws IOException
     * @throws RepositoryException
     */
    protected void updateOtherLanguages(Resource resource, Node bundle, File bundleDir)
            throws FileNotFoundException, IOException, RepositoryException {
        String bundleName = bundle.getName();

        File[] bundleFiles = bundleDir.listFiles(new BundleFilter(bundleName));
        if (bundleFiles.length == 0)
            return;
        //boolean entriesUpdated = false;
        for (File bundleFile : bundleFiles) {
            int languageStart = bundleFile.getName().indexOf("_");
            if (languageStart < 0)
                continue;
            /*if (!entriesUpdated) {
                readRepository();
                entriesUpdated = true;
            }*/

            int languageEnd = bundleFile.getName().lastIndexOf("_");
            int extensionStart = bundleFile.getName().indexOf(".properties");
            String language = languageStart != languageEnd ? bundleFile.getName().substring(languageStart + 1, languageEnd) : bundleFile.getName().substring(languageStart + 1, extensionStart);

            Properties bundleProperties = new Properties();
            bundleProperties.load(new FileInputStream(bundleFile));

            BundleEntries bundleEntries = this.bundleEntriesMap.get(bundleName);

            Enumeration propertyNames = bundleProperties.propertyNames();
            while (propertyNames.hasMoreElements()) {
                String propertyName = (String) propertyNames.nextElement();

                if ((!bundleEntries.containsName(propertyName))
                        || (((Node) bundleEntries.get(propertyName)).hasProperty(BundleConstants.NAMESPACE_NODES + BundleConstants.NAMESPACE_DELIMITER + language)))
                    continue;
                ((Node) bundleEntries.get(propertyName)).setProperty(BundleConstants.NAMESPACE_NODES + BundleConstants.NAMESPACE_DELIMITER + language, bundleProperties.getProperty(propertyName));
            }

            resource.addExclude(packagePathToFilePath(bundle.getProperty(BundleConstants.PROP_PACKAGE_PATH).getString()) + BundleConstants.PATH_SEPARATOR + bundleFile.getName());
        }
    }

    /**
     * Generate the bundles in the generated-resource folder from the
     * information in the repository.
     *
     * @throws RepositoryException
     * @throws FileNotFoundException
     * @throws IOException
     */
    protected void updateBundles()
            throws RepositoryException, FileNotFoundException, IOException {
        String resourceDir = this.outputDirectory.getAbsolutePath() + File.separator + GENERATED_RESOURCES;
        boolean isEmpty = true;

        for (BundleInfo bundleInfo : this.bundlesFound) {
            Node bundle = bundleInfo.getBundle();

            BundleEntries bundleEntries = this.bundleEntriesMap.get(bundle.getName());
            if (bundleEntries == null)
                continue;
            Node bundleEntry = getFirstEntry(bundleEntries);
            for (String repositoryLanguage : getPossibleLanguages(bundleEntry)) {
                Properties languageProperties = new Properties();
                Iterator<Node> entries = bundleEntries.getList().iterator();
                while (entries.hasNext()) {
                    bundleEntry = entries.next();
                    if (bundleEntry.hasProperty(repositoryLanguage))
                        languageProperties.setProperty(bundleEntry.getName(), bundleEntry.getProperty(repositoryLanguage).getString());
                }
                
                if (languageProperties.size() > 0) {
                    File bundleDir = new File(resourceDir + File.separator + packagePathToFilePath(bundle.getProperty(BundleConstants.PROP_PACKAGE_PATH).getString()));
                    bundleDir.mkdirs();
                    String language = "_" + repositoryLanguage.substring(repositoryLanguage.lastIndexOf(BundleConstants.NAMESPACE_DELIMITER) + 1);
                    if (repositoryLanguage.equals(BundleConstants.PROP_DEFAULT_LANGUAGE))
                        language = "";
                    File bundleFile = new File(bundleDir.getAbsolutePath() + File.separator + bundle.getName() + language + "." + "properties");
                    FileOutputStream out = new FileOutputStream(bundleFile);
                    languageProperties.store(out, null);
                    isEmpty = false;

                    getLog().info("Generated " + bundleFile.getPath());
                }
            }
        }

        if (!isEmpty) {
            Resource resource = new Resource();
            resource.setDirectory(resourceDir);
            this.project.addResource(resource);
        }
    }

    /**
     * Return the first entry of the hash map
     *
     * @param bundleEntries
     * @return
     */
    protected Node getFirstEntry(BundleEntries bundleEntries) {
        if (bundleEntries.getList().size() == 0)
            return null;
        
        return bundleEntries.getList().get(0);
    }

    /**
     * Add a new bundle entry to the repository from the updated bundle in
     * the resources folder.
     *
     * @param bundle
     * @param propertyName
     * @param value
     * @return the new bundle entry
     * @throws RepositoryException
     */
    protected Node addNewBundleEntry(Node bundle, String propertyName, String value)
            throws RepositoryException {
        Node bundleEntry = bundle.addNode(propertyName, BundleConstants.TYPENAME_BUNDLE_ENTRY);
        bundleEntry.setProperty(BundleConstants.PROP_DEFAULT_LANGUAGE, value);
        
        return bundleEntry;
    }

    /**
     * Retrieve the bundles referring to this module from the repository.
     * 
     * @return
     * @throws RepositoryException
     */
    protected Collection<Node> getBundles()
            throws RepositoryException {
        String queryString = this.searchPath + "//element(*," + BundleConstants.TYPENAME_BUNDLE + ")[@" + BundleConstants.PROP_MODULE + "='" + this.groupId + "." + this.artifactId + "']";
        QueryManager queryManager = this.session.getWorkspace().getQueryManager();
        Query query = queryManager.createQuery(queryString, Query.XPATH);
        NodeIterator result = query.execute().getNodes();
        LinkedList bundles = new LinkedList();
        while (result.hasNext()) {
            bundles.add(result.nextNode());
        }

        return bundles;
    }

    /**
     * Retrieve the bundle entries from the repository.
     *
     * @param bundle
     * @return
     * @throws RepositoryException
     */
    protected BundleEntries getBundleEntries(Node bundle)
            throws RepositoryException {
        BundleEntries bundleEntries = new BundleEntries();

        NodeIterator i = bundle.getNodes();
        while (i.hasNext()) {
            Node node = i.nextNode();
            if (node.isNodeType(BundleConstants.TYPENAME_BUNDLE_ENTRY))
                bundleEntries.add(node);
        }
        
        return bundleEntries;
    }

    /**
     * Determine the possible language from the given bundle entry.
     * 
     * @param bundleEntry
     * @return
     * @throws RepositoryException
     */
    protected Collection<String> getPossibleLanguages(Node bundleEntry)
            throws RepositoryException {
        Collection result = new LinkedList();

        NodeType type = bundleEntry.getPrimaryNodeType();
        PropertyDefinition[] propertyDefinitions = type.getDeclaredPropertyDefinitions();

        for (PropertyDefinition propertyDefinition : propertyDefinitions) {
            if ((propertyDefinition.getName().equals(BundleConstants.TYPENAME_BUNDLE)) || (propertyDefinition.getName().equals(BundleConstants.CHILD_DESCRIPTION)))
                continue;
            result.add(propertyDefinition.getName());
        }

        return result;
    }

    class BundleFilter
            implements FilenameFilter {

        String bundleName;

        public BundleFilter(String bundleName) {
            this.bundleName = bundleName;
        }

        public boolean accept(File dir, String name) {
            return (name.startsWith(this.bundleName)) && (name.endsWith("properties")) && (!name.equals(this.bundleName + "properties"));
        }
    }

    class BundleInfo {

        Resource resource;
        Node bundle;

        public BundleInfo(Resource resource, Node bundle) {
            this.resource = resource;
            this.bundle = bundle;
        }

        public Node getBundle() {
            return this.bundle;
        }

        public void setBundle(Node bundle) {
            this.bundle = bundle;
        }

        public Resource getResource() {
            return this.resource;
        }

        public void setResource(Resource resource) {
            this.resource = resource;
        }
    }
}
